angular.module('ui.bootstrap.dateparser', [])

    .service('uibDateParser', ['$log', '$locale', 'orderByFilter', function ($log, $locale, orderByFilter) {
        // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
        var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;

        var localeId;
        var formatCodeToRegex;

        this.init = function () {
            localeId = $locale.id;

            this.parsers = {};

            formatCodeToRegex = [
                {
                    key: 'yyyy',
                    regex: '\\d{4}',
                    apply: function (value) {
                        this.year = +value;
                    }
                },
                {
                    key: 'yy',
                    regex: '\\d{2}',
                    apply: function (value) {
                        this.year = +value + 2000;
                    }
                },
                {
                    key: 'y',
                    regex: '\\d{1,4}',
                    apply: function (value) {
                        this.year = +value;
                    }
                },
                {
                    key: 'M!',
                    regex: '0?[1-9]|1[0-2]',
                    apply: function (value) {
                        this.month = value - 1;
                    }
                },
                {
                    key: 'MMMM',
                    regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
                    apply: function (value) {
                        this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value);
                    }
                },
                {
                    key: 'MMM',
                    regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
                    apply: function (value) {
                        this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value);
                    }
                },
                {
                    key: 'MM',
                    regex: '0[1-9]|1[0-2]',
                    apply: function (value) {
                        this.month = value - 1;
                    }
                },
                {
                    key: 'M',
                    regex: '[1-9]|1[0-2]',
                    apply: function (value) {
                        this.month = value - 1;
                    }
                },
                {
                    key: 'd!',
                    regex: '[0-2]?[0-9]{1}|3[0-1]{1}',
                    apply: function (value) {
                        this.date = +value;
                    }
                },
                {
                    key: 'dd',
                    regex: '[0-2][0-9]{1}|3[0-1]{1}',
                    apply: function (value) {
                        this.date = +value;
                    }
                },
                {
                    key: 'd',
                    regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
                    apply: function (value) {
                        this.date = +value;
                    }
                },
                {
                    key: 'EEEE',
                    regex: $locale.DATETIME_FORMATS.DAY.join('|')
                },
                {
                    key: 'EEE',
                    regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|')
                },
                {
                    key: 'HH',
                    regex: '(?:0|1)[0-9]|2[0-3]',
                    apply: function (value) {
                        this.hours = +value;
                    }
                },
                {
                    key: 'hh',
                    regex: '0[0-9]|1[0-2]',
                    apply: function (value) {
                        this.hours = +value;
                    }
                },
                {
                    key: 'H',
                    regex: '1?[0-9]|2[0-3]',
                    apply: function (value) {
                        this.hours = +value;
                    }
                },
                {
                    key: 'h',
                    regex: '[0-9]|1[0-2]',
                    apply: function (value) {
                        this.hours = +value;
                    }
                },
                {
                    key: 'mm',
                    regex: '[0-5][0-9]',
                    apply: function (value) {
                        this.minutes = +value;
                    }
                },
                {
                    key: 'm',
                    regex: '[0-9]|[1-5][0-9]',
                    apply: function (value) {
                        this.minutes = +value;
                    }
                },
                {
                    key: 'sss',
                    regex: '[0-9][0-9][0-9]',
                    apply: function (value) {
                        this.milliseconds = +value;
                    }
                },
                {
                    key: 'ss',
                    regex: '[0-5][0-9]',
                    apply: function (value) {
                        this.seconds = +value;
                    }
                },
                {
                    key: 's',
                    regex: '[0-9]|[1-5][0-9]',
                    apply: function (value) {
                        this.seconds = +value;
                    }
                },
                {
                    key: 'a',
                    regex: $locale.DATETIME_FORMATS.AMPMS.join('|'),
                    apply: function (value) {
                        if (this.hours === 12) {
                            this.hours = 0;
                        }

                        if (value === 'PM') {
                            this.hours += 12;
                        }
                    }
                },
                {
                    key: 'Z',
                    regex: '[+-]\\d{4}',
                    apply: function (value) {
                        var matches = value.match(/([+-])(\d{2})(\d{2})/),
                            sign = matches[1],
                            hours = matches[2],
                            minutes = matches[3];
                        this.hours += toInt(sign + hours);
                        this.minutes += toInt(sign + minutes);
                    }
                },
                {
                    key: 'ww',
                    regex: '[0-4][0-9]|5[0-3]'
                },
                {
                    key: 'w',
                    regex: '[0-9]|[1-4][0-9]|5[0-3]'
                },
                {
                    key: 'GGGG',
                    regex: $locale.DATETIME_FORMATS.ERANAMES.join('|').replace(/\s/g, '\\s')
                },
                {
                    key: 'GGG',
                    regex: $locale.DATETIME_FORMATS.ERAS.join('|')
                },
                {
                    key: 'GG',
                    regex: $locale.DATETIME_FORMATS.ERAS.join('|')
                },
                {
                    key: 'G',
                    regex: $locale.DATETIME_FORMATS.ERAS.join('|')
                }
            ];
        };

        this.init();

        function createParser(format) {
            var map = [], regex = format.split('');

            // check for literal values
            var quoteIndex = format.indexOf('`');
            if (quoteIndex > -1) {
                var inLiteral = false;
                format = format.split('');
                for (var i = quoteIndex; i < format.length; i++) {
                    if (inLiteral) {
                        if (format[i] === '`') {
                            if (i + 1 < format.length && format[i + 1] === '\`') { // escaped backtick
                                format[i + 1] = '$';
                                regex[i + 1] = '';
                            } else { // end of literal
                                regex[i] = '';
                                inLiteral = false;
                            }
                        }
                        format[i] = '$';
                    } else {
                        if (format[i] === '`') { // start of literal
                            format[i] = '$';
                            regex[i] = '';
                            inLiteral = true;
                        }
                    }
                }

                format = format.join('');
            }

            angular.forEach(formatCodeToRegex, function (data) {
                var index = format.indexOf(data.key);

                if (index > -1) {
                    format = format.split('');

                    regex[index] = '(' + data.regex + ')';
                    format[index] = '$'; // Custom symbol to define consumed part of format
                    for (var i = index + 1, n = index + data.key.length; i < n; i++) {
                        regex[i] = '';
                        format[i] = '$';
                    }
                    format = format.join('');

                    map.push({
                        index: index,
                        apply: data.apply,
                        matcher: data.regex
                    });
                }
            });

            return {
                regex: new RegExp('^' + regex.join('') + '$'),
                map: orderByFilter(map, 'index')
            };
        }

        this.parse = function (input, format, baseDate) {
            if (!angular.isString(input) || !format) {
                return input;
            }

            format = $locale.DATETIME_FORMATS[format] || format;
            format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');

            if ($locale.id !== localeId) {
                this.init();
            }

            if (!this.parsers[format]) {
                this.parsers[format] = createParser(format);
            }

            var parser = this.parsers[format],
                regex = parser.regex,
                map = parser.map,
                results = input.match(regex),
                tzOffset = false;
            if (results && results.length) {
                var fields, dt;
                if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
                    fields = {
                        year: baseDate.getFullYear(),
                        month: baseDate.getMonth(),
                        date: baseDate.getDate(),
                        hours: baseDate.getHours(),
                        minutes: baseDate.getMinutes(),
                        seconds: baseDate.getSeconds(),
                        milliseconds: baseDate.getMilliseconds()
                    };
                } else {
                    if (baseDate) {
                        $log.warn('dateparser:', 'baseDate is not a valid date');
                    }
                    fields = {year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0};
                }

                for (var i = 1, n = results.length; i < n; i++) {
                    var mapper = map[i - 1];
                    if (mapper.matcher === 'Z') {
                        tzOffset = true;
                    }

                    if (mapper.apply) {
                        mapper.apply.call(fields, results[i]);
                    }
                }

                var datesetter = tzOffset ? Date.prototype.setUTCFullYear :
                    Date.prototype.setFullYear;
                var timesetter = tzOffset ? Date.prototype.setUTCHours :
                    Date.prototype.setHours;

                if (isValid(fields.year, fields.month, fields.date)) {
                    if (angular.isDate(baseDate) && !isNaN(baseDate.getTime()) && !tzOffset) {
                        dt = new Date(baseDate);
                        datesetter.call(dt, fields.year, fields.month, fields.date);
                        timesetter.call(dt, fields.hours, fields.minutes,
                            fields.seconds, fields.milliseconds);
                    } else {
                        dt = new Date(0);
                        datesetter.call(dt, fields.year, fields.month, fields.date);
                        timesetter.call(dt, fields.hours || 0, fields.minutes || 0,
                            fields.seconds || 0, fields.milliseconds || 0);
                    }
                }

                return dt;
            }
        };

        // Check if date is valid for specific month (and year for February).
        // Month: 0 = Jan, 1 = Feb, etc
        function isValid(year, month, date) {
            if (date < 1) {
                return false;
            }

            if (month === 1 && date > 28) {
                return date === 29 && (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0);
            }

            if (month === 3 || month === 5 || month === 8 || month === 10) {
                return date < 31;
            }

            return true;
        }

        function toInt(str) {
            return parseInt(str, 10);
        }
    }]);